package main

import (
	"context"
	"log"
	"runtime/debug"
	"sync"
	"time"

	"github.com/google/uuid"
)

////////////////////////////////////////////////////////////////
//////////////

type CronTaskStatus int

const (
	CronTaskStatus_Waiting CronTaskStatus = iota
	CronTaskStatus_Running
	CronTaskStatus_Cancel
	CronTaskStatus_Done
)

var statusString = [...]string{
	"waiting",
	"running",
	"cancel",
	"done",
}

func (this CronTaskStatus) String() string { return statusString[this] }

////////////////////////////////////////////////////////////////
//////////////

type CronTask struct {
	UUID   string
	Status CronTaskStatus
	STime  time.Time // start time
	FTime  time.Time // finish time

	mtx sync.Mutex

	Context context.Context
	Cancel  func()

	Callback           func(ctx context.Context) error
	TaskRunCallback    func()
	TaskDoneCallback   func(err error)
	TaskCancelCallback func()
}

func NewCronTask(stime time.Time, callback func(context.Context) error) *CronTask {
	c := &CronTask{
		STime:    stime,
		Callback: callback,
	}
	c.Context, c.Cancel = context.WithCancel(context.Background())
	return c
}

func (this *CronTask) SetTaskRunCallback(c func()) {
	this.mtx.Lock()
	this.TaskRunCallback = c
	this.mtx.Unlock()
}

func (this *CronTask) OnTaskRun() {
	this.Status = CronTaskStatus_Running
	if this.TaskRunCallback != nil {
		this.TaskRunCallback()
	}
}

func (this *CronTask) SetTaskDoneCallback(c func(error)) {
	this.mtx.Lock()
	this.TaskDoneCallback = c
	this.mtx.Unlock()
}

func (this *CronTask) OnTaskDone(err error) {
	this.FTime = time.Now()
	this.Status = CronTaskStatus_Done
	if this.TaskDoneCallback != nil {
		this.TaskDoneCallback(err)
	}
}

func (this *CronTask) SetTaskCancelCallback(c func()) {
	this.mtx.Lock()
	this.TaskCancelCallback = c
	this.mtx.Unlock()
}

func (this *CronTask) OnTaskCancel() {
	this.Status = CronTaskStatus_Cancel
	if this.TaskCancelCallback != nil {
		this.TaskCancelCallback()
	}
}

////////////////////////////////////////////////////////////////
//////////////

type CronTaskSchedule struct {
	alltask sync.Map
}

func (this *CronTaskSchedule) AddTask(t *CronTask) string {
	u := uuid.New().String()

	this.alltask.Store(u, t)

	go func(t *CronTask) {
		defer func() {
			if err := recover(); err != nil {
				log.Println("oops, crontask:", t.UUID, "panic. error:", err)
				debug.PrintStack()
			}
		}()

		var d time.Duration
		n := time.Now()
		if t.STime.After(n) {
			d = t.STime.Sub(n)
		}

		select {
		case <-t.Context.Done():
			t.OnTaskCancel()

		case <-time.After(d):
			t.OnTaskRun()
			err := t.Callback(t.Context)
			switch err {
			case context.Canceled:
				t.OnTaskCancel()
			default:
				t.OnTaskDone(err)
			}
		}
	}(t)

	return u
}

func (this *CronTaskSchedule) CancelTask(u string) {
	v, ok := this.alltask.Load(u)
	if !ok {
		return
	}

	t := v.(*CronTask)
	t.Cancel()
	this.alltask.Delete(u)
}

////////////////////////////////////////////////////////////////
////////////// wrapper

var defaultSchedule = &CronTaskSchedule{}

func AddTask(t *CronTask) string { return defaultSchedule.AddTask(t) }
func CancelTask(u string)        { defaultSchedule.CancelTask(u) }
